package cosc455.project1;

import java.util.ArrayList;

/**
 * The syntax analyzer. It implements the BNF grammar for LOLcode recursively.
 * It passes the tokens into a String ArrayList.
 * 
 * @author BinhDang
 *
 */
public class SyntaxAnalyzer implements SyntaxAnalyzerInterface {
	// flag marker for errors
	public static boolean errorFlag;

	// String ArrayList of updated tokens
	public static ArrayList<String> syntax = new ArrayList<String>();

	// A scope level counter
	private int variableScope = 0;

	// An ArrayList that holds nodes with variable definitions
	private ArrayList<VarDef> variable = new ArrayList<VarDef>();

	/*
	 * (non-Javadoc)
	 * 
	 * @see cosc455.project1.SyntaxAnalyzerInterface#lolcode()
	 */
	@Override
	public void lolcode() {
		resetError();

		// Output error if file does not begin with #HAI
		if (!Compiler.currentToken.equals("#HAI")) {
			setError();
			System.err
					.println("ERROR: Source file must begin with #HAI annotation");
		}

		Compiler.currentToken = "#StartDocument";
		if (!errorFlag)
			nextTokenAdd();

		// Output error if file has a second #HAI tag
		if (Compiler.currentToken.equals("#HAI")) {
			setError();
			System.err.println("ERROR: #HAI annotation can not be repeated");
		}

		checkEndTag();
		if (!errorFlag)
			comment();
		checkEndTag();
		if (!errorFlag)
			define();
		checkEndTag();
		if (!errorFlag)
			head();
		checkEndTag();
		if (!errorFlag)
			body();
		checkEndTag();

		// Output error if file doesn't end with #KTHXBYE
		if (!errorFlag) {
			if (!Compiler.currentToken.equals("#KTHXBYE")) {
				setError();
				System.err
						.println("ERROR: Source file must end with #KTHXBYE annotation");
			}
		}

		checkEnd();
		Compiler.currentToken = "#EndDocument";
		syntax.add(Compiler.currentToken);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see cosc455.project1.SyntaxAnalyzerInterface#comment()
	 */
	@Override
	public void comment() {
		if (Compiler.currentToken.equals("#OBTW")) {
			Compiler.currentToken = "#StartComment";
			nextTokenAdd();
			fillText();
			if (!Compiler.currentToken.equals("#TLDR")) {
				setError();
				System.err.println("Syntax Error: " + Compiler.currentToken
						+ " is not valid\n #TLDR annotation expected");
			}
			Compiler.currentToken = "#EndComment";
			nextTokenAdd();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see cosc455.project1.SyntaxAnalyzerInterface#head()
	 */
	@Override
	public void head() {
		if (Compiler.currentToken.equals("#MAEK HEAD")) {
			Compiler.currentToken = "#StartHeading";
			nextTokenAdd();
			title();
			if (!Compiler.currentToken.equals("#OIC")) {
				setError();
				System.err.println("Syntax Error: " + Compiler.currentToken
						+ " is not valid\n #OIC annotation expected");
			}
			Compiler.currentToken = "#EndHeading";
			nextTokenAdd();
		}

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see cosc455.project1.SyntaxAnalyzerInterface#title()
	 */
	@Override
	public void title() {
		if (Compiler.currentToken.equals("#GIMMEH TITLE")) {
			Compiler.currentToken = "#StartTitle";
			nextTokenAdd();
			fillText();
			if (!Compiler.currentToken.equals("#MKAY")) {
				setError();
				System.err.println("Syntax Error: " + Compiler.currentToken
						+ " is not valid\n #MKAY annotation expected");
			}
			Compiler.currentToken = "#EndTitle";
			nextTokenAdd();
		}
	}

	/**
	 * This method implements the BNF grammar recursively for: comments,
	 * paragraphs, bold, italics, lists.
	 */
	public void body() {
		fillText();

		String str = Compiler.currentToken;
		switch (str) {
		case "#OBTW":
			comment();
			body();
			break;
		case "#VISIBLE":
		case "#GIMMEH NEWLINE":
		case "#GIMMEH SOUNDZ":
		case "#GIMMEH VIDZ":
			innerText();
			body();
			break;
		case "#MAEK PARAGRAF":
			paragraph();
			body();
			break;
		case "#GIMMEH BOLD":
			bold();
			body();
			break;
		case "#GIMMEH ITALICS":
			italics();
			body();
			break;
		case "#MAEK LIST":
			list();
			break;
		default:
			break;
		}
	}

	/**
	 * This method implements the BNF grammar recursively for: variable
	 * definitions, text, newline, audio, video
	 */
	public void innerText() {
		fillText();
		String str = Compiler.currentToken;
		switch (str) {
		case "#VISIBLE":
			use();
			innerText();
			break;
		case "#GIMMEH NEWLINE":
			newLine();
			innerText();
			break;
		case "#GIMMEH SOUNDZ":
			audio();
			innerText();
			break;
		case "#GIMMEH VIDZ":
			video();
			innerText();
			break;
		default:
			break;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see cosc455.project1.SyntaxAnalyzerInterface#paragraph()
	 */
	@Override
	public void paragraph() {
		if (Compiler.currentToken.equals("#MAEK PARAGRAF")) {
			Compiler.currentToken = "#StartParagraph";
			variableScope++;
			while (!Compiler.currentToken.equals("#OIC")) {
				nextTokenAdd();
				define();
				innerParagraph();
			}

			if (!Compiler.currentToken.equals("#OIC")) {
				setError();
				System.err.println("Syntax Error @paragraph: "
						+ Compiler.currentToken
						+ " is not valid\n #OIC annotation expected");
			}
			Compiler.currentToken = "#EndParagraph";
			nextTokenAdd();
			variableScope--;
		}

	}

	/**
	 * This method implements the BNF grammar recursively for all possible tags
	 * allowed between paragraph tags
	 */
	public void innerParagraph() {
		innerText();
		String str = Compiler.currentToken;
		switch (str) {
		case "#GIMMEH BOLD":
			bold();
			innerParagraph();
			break;
		case "#GIMMEH ITALICS":
			italics();
			innerParagraph();
			break;
		case "#MAEK LIST":
			list();
			innerParagraph();
			break;
		case "#GIMMEH NEWLINE":
			newLine();
			innerParagraph();
			break;
		default:
			break;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see cosc455.project1.SyntaxAnalyzerInterface#bold()
	 */
	@Override
	public void bold() {
		if (Compiler.currentToken.equals("#GIMMEH BOLD")) {
			Compiler.currentToken = "#StartBold";
			variableScope++;
			nextTokenAdd();
			define();
			innerText();
			if (!Compiler.currentToken.equals("#MKAY")) {
				setError();
				System.err.println("Syntax Error @BOLD: "
						+ Compiler.currentToken
						+ " is not valid\n #MKAY annotation expected");
			}
			Compiler.currentToken = "#EndBold";
			nextTokenAdd();
			variableScope--;
		}

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see cosc455.project1.SyntaxAnalyzerInterface#italics()
	 */
	@Override
	public void italics() {
		if (Compiler.currentToken.equals("#GIMMEH ITALICS")) {
			Compiler.currentToken = "#StartItalics";
			variableScope++;
			nextTokenAdd();
			define();
			innerText();
			if (!Compiler.currentToken.equals("#MKAY")) {
				setError();
				System.err.println("Syntax Error @ITALICS: "
						+ Compiler.currentToken
						+ " is not valid\n #MKAY annotation expected");
			}
			Compiler.currentToken = "#EndItalics";
			nextTokenAdd();
			variableScope--;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see cosc455.project1.SyntaxAnalyzerInterface#list()
	 */
	@Override
	public void list() {
		if (Compiler.currentToken.equals("#MAEK LIST")) {
			Compiler.currentToken = "#StartList";
			nextTokenAdd();
			if (!Compiler.currentToken.equals("#GIMMEH ITEM")) {
				setError();
				System.err.println("Syntax Error @list: "
						+ Compiler.currentToken
						+ " is not valid\n #GIMMEH ITEM annotation expected");
			}
			while (!Compiler.currentToken.equals("#OIC")) {
				item();
			}
			if (!Compiler.currentToken.equals("#OIC")) {
				setError();
				System.err.println("Syntax Error @listEnd: "
						+ Compiler.currentToken
						+ " is not valid\n #OIC annotation expected");
			}
			Compiler.currentToken = "#EndList";
			nextTokenAdd();
		}
	}

	/**
	 * This method implements BNF grammar recursively for all possible tags
	 * allowed in list
	 */
	public void innerList() {
		innerText();
		String str = Compiler.currentToken;
		switch (str) {
		case "#GIMMEH BOLD":
			bold();
			innerList();
			break;
		case "#GIMMEH ITALICS":
			italics();
			innerList();
			break;
		case "#MAEK LIST":
			list();
			innerList();
			break;
		default:
			break;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see cosc455.project1.SyntaxAnalyzerInterface#item()
	 */
	@Override
	public void item() {
		if (Compiler.currentToken.equals("#GIMMEH ITEM")) {
			Compiler.currentToken = "#StartItem";
			nextTokenAdd();
			innerList();
			if (!Compiler.currentToken.equals("#MKAY")) {
				setError();
				System.err.println("Syntax Error @item: "
						+ Compiler.currentToken
						+ " is not valid\n #MKAY annotation expected");
			}
			Compiler.currentToken = "#EndItem";
			nextTokenAdd();
		}
	}

	/**
	 * This method implements the newline tag
	 */
	public void newLine() {
		if (Compiler.currentToken.equals("#GIMMEH NEWLINE")) {
			Compiler.currentToken = "#StartNewline";
			nextTokenAdd();
			while (Compiler.currentToken.equals("#GIMMEH NEWLINE")) {
				newLine();
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see cosc455.project1.SyntaxAnalyzerInterface#audio()
	 */
	@Override
	public void audio() {
		if (Compiler.currentToken.equals("#GIMMEH SOUNDZ")) {
			Compiler.currentToken = "#StartAudio";
			nextTokenAdd();
			fillText();
			if (!Compiler.currentToken.equals("#MKAY")) {
				setError();
				System.err.println("Syntax Error @audio: "
						+ Compiler.currentToken
						+ " is not valid\n #MKAY annotation expected");
			}
			Compiler.currentToken = "#EndAudio";
			nextTokenAdd();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see cosc455.project1.SyntaxAnalyzerInterface#video()
	 */
	@Override
	public void video() {
		if (Compiler.currentToken.equals("#GIMMEH VIDZ")) {
			Compiler.currentToken = "#StartVideo";
			nextTokenAdd();
			fillText();
			if (!Compiler.currentToken.equals("#MKAY")) {
				setError();
				System.err.println("Syntax Error @video: "
						+ Compiler.currentToken
						+ " is not valid\n #MKAY annotation expected");
			}
			Compiler.currentToken = "#EndVideo";
			nextTokenAdd();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see cosc455.project1.SyntaxAnalyzerInterface#define()
	 */
	@Override
	public void define() {
		if (Compiler.currentToken.equals("#I HAS A")) {
			Compiler.Lexer.getNextToken();
			String original = Compiler.currentToken;
			Compiler.Lexer.getNextToken();
			if (!Compiler.currentToken.equals("#ITZ")) {
				setError();
				System.err.println("Syntax Error @define: "
						+ Compiler.currentToken
						+ " is not valid\n #ITZ annotation expected");
			}
			Compiler.currentToken = "#Definition";
			Compiler.Lexer.getNextToken();
			String def = Compiler.currentToken;

			VarDef varData = new VarDef(original, def, variableScope);
			variable.add(varData);
			Compiler.Lexer.getNextToken();
			if (!Compiler.currentToken.equals("#MKAY")) {
				setError();
				System.err.println("Syntax Error @define: "
						+ Compiler.currentToken
						+ " is not valid\n #MKAY annotation expected");
			}
			Compiler.currentToken = "#EndDefine";
			Compiler.Lexer.getNextToken();
			if (Compiler.currentToken.equals("#I HAS A")) {
				define();
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see cosc455.project1.SyntaxAnalyzerInterface#use()
	 */
	@Override
	public void use() {
		if (Compiler.currentToken.equals("#VISIBLE")) {
			Compiler.currentToken = "#StartUse";
			Compiler.Lexer.getNextToken();
			if (checkText())
				varReplace(Compiler.currentToken);
			Compiler.Lexer.getNextToken();
			if (!Compiler.currentToken.equals("#MKAY")) {
				setError();
				System.err.println("Syntax Error @variable-use: "
						+ Compiler.currentToken
						+ " is not valid\n #MKAY annotation expected");
			}
			Compiler.currentToken = "#EndUse";
			Compiler.Lexer.getNextToken();
		}
	}

	/**
	 * This method searches an ArrayList<VarDef> a node containing the same old
	 * definition and adds the new definition. If not found then prints
	 * undefined variable error
	 * 
	 * @param s
	 *            String being replaced with new definition
	 */
	public void varReplace(String s) {
		int marker = 0;
		boolean found = false;
		for (VarDef v : variable) {
			if ((v.original.equals(s)) && (variableScope >= v.scope)) {
				if (marker > 0) {
					syntax.set(syntax.size() - 1, v.def);
				} else {
					syntax.add(v.def);
					found = true;
					marker++;
				}

			}
		}
		if (!found) {
			System.err.println("Semantic Error " + Compiler.currentToken
					+ " is undefined");
			setError();
		}
	}

	/**
	 * * @return true if current token is text; false if current token is an
	 * annotation
	 */
	public boolean checkText() {
		if (!LexicalAnalyzer.isAnnotate(Compiler.currentToken)) {
			return true;
		} else {
			setError();
			System.err
					.println("SYNTAX ERROR: Token is an annoatation\nText expected");
			return false;
		}
	}

	/**
	 * This method collects the texts (non annotation tags) in a loop
	 */
	public void fillText() {
		while (!LexicalAnalyzer.isAnnotate(Compiler.currentToken)) {
			nextTokenAdd();
		}
	}

	/**
	 * This method adds the current token to a String ArrayList and gets the
	 * next token
	 */
	public void nextTokenAdd() {
		syntax.add(Compiler.currentToken);
		Compiler.Lexer.getNextToken();
	}

	/**
	 * Sets error flag marker to true
	 */
	static void setError() {
		errorFlag = true;
	}

	/**
	 * Sets error flag marker to false
	 */
	void resetError() {
		errorFlag = false;
	}

	/**
	 * @return boolean errorFlag
	 */
	boolean getError() {
		return errorFlag;
	}

	/**
	 * This method handles invalid end annotations and prints error
	 */
	public void checkEndTag() {
		String str = Compiler.currentToken;
		switch (str) {
		case "#OIC":
		case "#MKAY":
		case "#TLDR":
		case "#ITZ":
			setError();
			System.err.println("SYNTAX ERROR: " + Compiler.currentToken
					+ " is not valid\n Can not start with an End Annotation");
			break;
		}
	}

	/**
	 * This method handles annotations after the end of file and prints error
	 * message. It also removes the last white space.
	 */
	public void checkEnd() {
		if (!errorFlag) {
			if (Compiler.currentToken.equals("#KTHXBYE")) {
				nextTokenAdd();
			}
			if (!Compiler.currentToken.equals("#KTHXBYE")
					&& !Compiler.currentToken.equals("\n")) {
				setError();
				System.err
						.println("ERROR: Can not have another annotation after source file ends");
			}
			syntax.remove(syntax.size() - 1);
		}
	}

}
